CDK for TerraformでGithub Actionsを使ったCDKデプロイパイプラインを作ってみた
CX事業部Delivery部の新澤です。
CDKのデプロイをGithub Actionsで実行するパイプラインを作成するにあたって、AWS側とGithub側、両方のリソース作成をコードでいっぺんに終わらせてしまいたかったので、せっかくなので2022年8月にGAされたCDK for Terraform(CDKTF)を使って試してみました。
やってみた
作成するリソースは以下になります。
- Github
- Githubリポジトリ
- Github Actions シークレット
- AWS
- IAM IDプロバイダー
- IAM Role
準備
CDKTFの初期化を実行します。
いくつかの質問に回答すると、依存ライブラリのインストールが開始されます。
$ cdktf init --template=typescript --local Note: By supplying '--local' option you have chosen local storage mode for storing the state of your stack. This means that your Terraform state file will be stored locally on disk in a file 'terraform.<STACK NAME>.tfstate' in the root of your project. ? Project Name cdktf-github-actions-cicd-sample ? Project Description A simple getting started project for cdktf. ? Do you want to start from an existing Terraform project? No ? Do you want to send crash reports to the CDKTF team? See https://www.terraform.io/cdktf/create-and-deploy/configuration-file#enable-crash-reporting-for-the-cli for more information Yes added 2 packages, and audited 57 packages in 576ms 5 packages are looking for funding run `npm fund` for details found 0 vulnerabilities added 297 packages, and audited 354 packages in 2s 33 packages are looking for funding run `npm fund` for details found 0 vulnerabilities ======================================================================================================== Your cdktf typescript project is ready! cat help Print this message Compile: npm run get Import/update Terraform providers and modules (you should check-in this directory) npm run compile Compile typescript code to javascript (or "npm run watch") npm run watch Watch for changes and compile typescript in the background npm run build Compile typescript Synthesize: cdktf synth [stack] Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply') Diff: cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: cdktf deploy [stack] Deploy the given stack Destroy: cdktf destroy [stack] Destroy the stack Test: npm run test Runs unit tests (edit __tests__/main-test.ts to add your own tests) npm run test:watch Watches the tests and reruns them on change Upgrades: npm run upgrade Upgrade cdktf modules to latest version npm run upgrade:next Upgrade cdktf modules to latest "@next" version (last commit) Use Providers: You can add prebuilt providers (if available) or locally generated ones using the add command: cdktf provider add "aws@~>3.0" null kreuzwerker/docker You can find all prebuilt providers on npm: https://www.npmjs.com/search?q=keywords:cdktf You can also install these providers directly through npm: npm install @cdktf/provider-aws npm install @cdktf/provider-google npm install @cdktf/provider-azurerm npm install @cdktf/provider-docker npm install @cdktf/provider-github npm install @cdktf/provider-null You can also build any module or provider locally. Learn more https://cdk.tf/modules-and-providers
ライブラリのインストールが終わるとインストールするTerraformプロバイダーの選択肢が現れます。
今回は、aws, github, tlsの3つを選択します。
? What providers do you want to use? aws, github, tls Checking whether pre-built provider exists for the following constraints: provider: aws version : latest language: typescript cdktf : 0.14.1 Found pre-built provider. Adding package @cdktf/provider-aws @ 11.0.0 Installing package @cdktf/provider-aws @ 11.0.0 using npm. Package installed. Checking whether pre-built provider exists for the following constraints: provider: integrations/github version : latest language: typescript cdktf : 0.14.1 Found pre-built provider. Adding package @cdktf/provider-github @ 4.0.0 Installing package @cdktf/provider-github @ 4.0.0 using npm. Package installed. Checking whether pre-built provider exists for the following constraints: provider: tls version : latest language: typescript cdktf : 0.14.1 Found pre-built provider. Adding package @cdktf/provider-tls @ 4.0.0 Installing package @cdktf/provider-tls @ 4.0.0 using npm. Package installed. $ tree -L 1 \. ├── __tests__ ├── cdktf.json ├── help ├── jest.config.js ├── main.ts ├── node_modules ├── package-lock.json ├── package.json ├── setup.js └── tsconfig.json
CDKの作成
まず、最初にIAM IDプロバイダーを作成します。Github ActionsからOIDC認証したいのでOIDC用IDプロバイダーを作成してみます。
import { IamOpenidConnectProvider } from "@cdktf/provider-aws/lib/iam-openid-connect-provider"; import { dataTlsCertificate } from "@cdktf/provider-tls"; import { TlsProvider } from "@cdktf/provider-tls/lib/provider"; import { Construct } from "constructs"; export class AwsOidcProvider extends Construct { readonly oidcProviderArn: string; constructor(scope: Construct, id: string) { super(scope, id); // TLS Provider new TlsProvider(this, 'TLS'); // IAM OIDC Provider const githubCert = new dataTlsCertificate.DataTlsCertificate(this, 'GithubCertificate', { url: 'https://token.actions.githubusercontent.com/.well-known/openid-configuration', }); const oidcProvider = new IamOpenidConnectProvider(this, 'AwsOidcProvider', { url: 'https://token.actions.githubusercontent.com', clientIdList: ["sts.amazonaws.com"], thumbprintList: [githubCert.certificates.get(0).sha1Fingerprint], }); this.oidcProviderArn = oidcProvider.arn; } }
OIDC認証用IDプロバイダーの作成時にOIDCプロバイダのCAの証明書のハッシュ値(thumbprint)を指定する必要があるのですが、こちらの記事で紹介されているTerraformの実装を参考にCDKTFに書き換えて実装しています。
const githubCert = new dataTlsCertificate.DataTlsCertificate(this, 'GithubCertificate', { url: 'https://token.actions.githubusercontent.com/.well-known/openid-configuration', }); const oidcProvider = new IamOpenidConnectProvider(this, 'AwsOidcProvider', { url: 'https://token.actions.githubusercontent.com', clientIdList: ["sts.amazonaws.com"], thumbprintList: [githubCert.certificates.get(0).sha1Fingerprint], });
次はリポジトリと、リポジトリに対応するIAM Role、Role ARNを格納するGithub Actionsシークレットを作ります。 こちらは、リポジトリ毎に複数作成することもあるかと思い、コンストラクトにまとめてみました。
IAM Roleの設定内容は、Github公式ドキュメントの内容に沿ったものにしています。
import { DataAwsIamPolicyDocument } from "@cdktf/provider-aws/lib/data-aws-iam-policy-document"; import { IamRole } from "@cdktf/provider-aws/lib/iam-role"; import { ActionsSecret } from "@cdktf/provider-github/lib/actions-secret"; import { Repository } from "@cdktf/provider-github/lib/repository"; import { Construct } from "constructs"; type GithubRepositoryWithActionsAwsOidcProps = { oidcProviderArn: string, repositoryName: string, repositoryDescription: string, repositoryBranchNane: string, githubActionsSecretName?: string, } export class GithubRepositoryWithActionsAwsOidc extends Construct { constructor(scope: Construct, id: string, props: GithubRepositoryWithActionsAwsOidcProps) { super(scope, id); // Github Repository const repository = new Repository(this, 'Repository', { name: props.repositoryName, description: props.repositoryDescription, visibility: 'private' }) // IAM Role const assumeRolePolicyDoc = new DataAwsIamPolicyDocument(this, 'AwsOidcAssumeRolePolicy', { statement: [{ effect: 'Allow', principals: [ { type: "Federated", identifiers: [ props.oidcProviderArn ], } ], actions: ['sts:AssumeRoleWithWebIdentity'], condition: [ { test: 'StringEquals', variable: 'token.actions.githubusercontent.com:aud', values: ['sts.amazonaws.com'] }, { test: 'StringEquals', variable: 'token.actions.githubusercontent.com:sub', values: [`repo:${repository.fullName}:ref:refs/heads/${props.repositoryBranchNane}`] }, ] }], }); const oidcRole = new IamRole(this, 'AwsOidcRole', { name: 'sample-github-actions-role', assumeRolePolicy: assumeRolePolicyDoc.json, managedPolicyArns: ['arn:aws:iam::aws:policy/AdministratorAccess'], }); // Github Actions Secret new ActionsSecret(this, 'ActionsEnvSecret', { repository: repository.name, secretName: props.githubActionsSecretName || 'AWS_OIDC_ROLE_ARN', plaintextValue: oidcRole.arn, }); } }
最後に上記をmain.tsで呼び出します。
実行時に必要なAWSクレデンシャル情報とGithubトークンは、環境変数で提供する想定にしています。
import { AwsProvider } from "@cdktf/provider-aws/lib/provider"; import { GithubProvider } from "@cdktf/provider-github/lib/provider"; import { App, TerraformStack } from "cdktf"; import { Construct } from "constructs"; import { AwsOidcProvider } from "./lib/aws-oidc-provider"; import { GithubRepositoryWithActionsAwsOidc } from "./lib/github-repository-with-actions-aws-oidc"; class GithubActionsCDKPipeline extends TerraformStack { constructor(scope: Construct, id: string) { super(scope, id); // Github Provider new GithubProvider(this, "Github", { token: process.env.GITHUB_TOKEN, }); // AWS Provider new AwsProvider(this, "AWS"); const oidcProvider = new AwsOidcProvider(this, "AwsOidcProvider"); new GithubRepositoryWithActionsAwsOidc(this, 'Resources', { repositoryName: 'sample-repo', repositoryDescription: 'sample repository', repositoryBranchNane: 'main', oidcProviderArn: oidcProvider.oidcProviderArn, }); } } const app = new App(); new GithubActionsCDKPipeline(app, "cdktf-github-actions-cicd-sample"); app.synth();
デプロイ
それでは、作成したCDKスタックをデプロイしてみます。
$ export GITHUB_TOKEN='xxxxxxxxxxxxxxxxxx' $ export | grep AWS_ AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxx AWS_DEFAULT_REGION=ap-northeast-1 AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxx $ cdktf deploy cdktf-github-actions-cicd-sample Initializing the backend... cdktf-github-actions-cicd-sample Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file cdktf-github-actions-cicd-sample - Reusing previous version of integrations/github from the dependency lock file cdktf-github-actions-cicd-sample - Reusing previous version of hashicorp/tls from the dependency lock file cdktf-github-actions-cicd-sample - Using previously-installed hashicorp/aws v4.39.0 cdktf-github-actions-cicd-sample - Using previously-installed integrations/github v4.31.0 cdktf-github-actions-cicd-sample - Using previously-installed hashicorp/tls v4.0.4 cdktf-github-actions-cicd-sample Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. cdktf-github-actions-cicd-sample data.tls_certificate.AwsOidcProvider_GithubCertificate_1922994C (AwsOidcProvider/GithubCertificate): Reading... cdktf-github-actions-cicd-sample data.tls_certificate.AwsOidcProvider_GithubCertificate_1922994C (AwsOidcProvider/GithubCertificate): Read complete after 0s [id=2f98b9dddcf0778622dc6788373a7f8c02e3a2c3] cdktf-github-actions-cicd-sample Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create <= read (data resources) Terraform will perform the following actions: cdktf-github-actions-cicd-sample # data.aws_iam_policy_document.Resources_AwsOidcAssumeRolePolicy_BF028658 (Resources/AwsOidcAssumeRolePolicy) will be read during apply # (config refers to values not yet known) <= data "aws_iam_policy_document" "Resources_AwsOidcAssumeRolePolicy_BF028658" { + id = (known after apply) + json = (known after apply) + statement { + actions = [ + "sts:AssumeRoleWithWebIdentity", ] + effect = "Allow" + condition { + test = "StringEquals" + values = [ + "sts.amazonaws.com", ] + variable = "token.actions.githubusercontent.com:aud" } + condition { + test = "StringEquals" + values = [ + (known after apply), ] + variable = "token.actions.githubusercontent.com:sub" } + principals { + identifiers = [ + (known after apply), ] + type = "Federated" } } } # aws_iam_openid_connect_provider.AwsOidcProvider_F379C144 (AwsOidcProvider/AwsOidcProvider) will be created + resource "aws_iam_openid_connect_provider" "AwsOidcProvider_F379C144" { + arn = (known after apply) + client_id_list = [ + "sts.amazonaws.com", ] + id = (known after apply) + tags_all = (known after apply) + thumbprint_list = [ + "6938fd4d98bab03faadb97b34396831e3780aea1", ] + url = "https://token.actions.githubusercontent.com" } # aws_iam_role.Resources_AwsOidcRole_8745526F (Resources/AwsOidcRole) will be created + resource "aws_iam_role" "Resources_AwsOidcRole_8745526F" { + arn = (known after apply) + assume_role_policy = (known after apply) + create_date = (known after apply) + force_detach_policies = false + id = (known after apply) + managed_policy_arns = [ + "arn:aws:iam::aws:policy/AdministratorAccess", ] + max_session_duration = 3600 + name = "sample-github-actions-role" + name_prefix = (known after apply) + path = "/" + tags_all = (known after apply) + unique_id = (known after apply) + inline_policy { + name = (known after apply) + policy = (known after apply) } } # github_actions_secret.Resources_ActionsEnvSecret_522D50A7 (Resources/ActionsEnvSecret) will be created + resource "github_actions_secret" "Resources_ActionsEnvSecret_522D50A7" { + created_at = (known after apply) + id = (known after apply) + plaintext_value = (sensitive value) + repository = "sample-repo" + secret_name = "AWS_OIDC_ROLE_ARN" + updated_at = (known after apply) } # github_repository.Resources_Repository_0F8A5956 (Resources/Repository) will be created + resource "github_repository" "Resources_Repository_0F8A5956" { + allow_auto_merge = false + allow_merge_commit = true + allow_rebase_merge = true + allow_squash_merge = true + archived = false + branches = (known after apply) + default_branch = (known after apply) + delete_branch_on_merge = false + description = "sample repository" + etag = (known after apply) + full_name = (known after apply) + git_clone_url = (known after apply) + html_url = (known after apply) + http_clone_url = (known after apply) + id = (known after apply) + merge_commit_message = "PR_TITLE" + merge_commit_title = "MERGE_MESSAGE" + name = "sample-repo" + node_id = (known after apply) + private = (known after apply) + repo_id = (known after apply) + squash_merge_commit_message = "COMMIT_MESSAGES" + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + ssh_clone_url = (known after apply) + svn_url = (known after apply) + visibility = "private" } Plan: 4 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for cdktf-github-actions-cicd-sample ❯ Approve Applies the changes outlined in the plan. Dismiss Stop
特に問題が無ければ、Approveを選択します。
cdktf-github-actions-cicd-sample github_repository.Resources_Repository_0F8A5956 (Resources/Repository): Creating... cdktf-github-actions-cicd-sample aws_iam_openid_connect_provider.AwsOidcProvider_F379C144 (AwsOidcProvider/AwsOidcProvider): Creating... cdktf-github-actions-cicd-sample aws_iam_openid_connect_provider.AwsOidcProvider_F379C144 (AwsOidcProvider/AwsOidcProvider): Creation complete after 1s [id=arn:aws:iam::xxxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com] cdktf-github-actions-cicd-sample github_repository.Resources_Repository_0F8A5956 (Resources/Repository): Creation complete after 6s [id=sample-repo] cdktf-github-actions-cicd-sample data.aws_iam_policy_document.Resources_AwsOidcAssumeRolePolicy_BF028658 (Resources/AwsOidcAssumeRolePolicy): Reading... cdktf-github-actions-cicd-sample data.aws_iam_policy_document.Resources_AwsOidcAssumeRolePolicy_BF028658 (Resources/AwsOidcAssumeRolePolicy): Read complete after 0s [id=1632601266] cdktf-github-actions-cicd-sample aws_iam_role.Resources_AwsOidcRole_8745526F (Resources/AwsOidcRole): Creating... cdktf-github-actions-cicd-sample aws_iam_role.Resources_AwsOidcRole_8745526F (Resources/AwsOidcRole): Creation complete after 2s [id=sample-github-actions-role] cdktf-github-actions-cicd-sample github_actions_secret.Resources_ActionsEnvSecret_522D50A7 (Resources/ActionsEnvSecret): Creating... cdktf-github-actions-cicd-sample github_actions_secret.Resources_ActionsEnvSecret_522D50A7 (Resources/ActionsEnvSecret): Creation complete after 2s [id=sample-repo:AWS_OIDC_ROLE_ARN] cdktf-github-actions-cicd-sample Apply complete! Resources: 4 added, 0 changed, 0 destroyed. No outputs found.
デプロイできました!
CDKデプロイパイプラインを動かしてみる
作成したリポジトリを使ってCDKデプロイしてみます。
こちらはCDKTFではなく、AWS CDK v2を使用したものになります。
内容はS3バケットを1つデプロイするだけの簡単なスタックです。
import * as cdk from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { Queue } from 'aws-cdk-lib/aws-sqs'; import { Construct } from 'constructs'; export class AwsCdkGithubActionsCicdSampleStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); new Bucket(this, 'SampleBAwsCdkGithubActionsCicdSampleBucket', { bucketName: `sample-bucket-${this.account}`, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); } }
Github Actionsの定義は以下のようにしました。
on: push: branches: - main paths: - 'bin/**' - 'lib/**' permissions: id-token: write contents: read env: AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }} AWS_REGION: ap-northeast-1 jobs: aws-deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Assume Role uses: aws-actions/configure-aws-credentials@v1-node16 with: role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }} aws-region: ${{env.AWS_REGION}} - name: Cache CDK Dependency uses: actions/cache@v3 id: cache_cdk_dependency_id env: cache-name: cache-cdk-dependency with: path: node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }} restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}- - name: Install CDK Dependency if: ${{ steps.cache_cdk_dependency_id.outputs.cache-hit != 'true' }} run: npm install - name: Deploy run: npm run deploy
CDKスクリプトを先ほど作成したリポジトリにプッシュして、動作を確認してみます。
Github Actionsが正常に実行されました。
S3バケットも作成されています。
$ aws s3 ls | grep sample-bucket 2022-11-22 16:48:48 sample-bucket-xxxxxxxxxxx
最後に
GithubリポジトリからGithub ActionsでCDKデプロイを行うパイプラインを作成する際に必要な設定をCDK for Terraformを使って一気にやってみました。
AWS以外のリソースも含めてコードで管理しようとするとTerraformを利用することが多いと思いますが、私のようにTerraformにあまり馴染みがないけどAWS CDKは普段から活用しているというような場合には、CDK for Terraformは非常にとっつきやすいのではないでしょうか?